前面寫這麼多糟糕事,應該不會更糟糕了吧?
8 月底主管特別交代,要我研究伺服器監控。
我心想:這不難嘛,以前在大同大學機房就寫過監控腳本,直接搬出來改一改就能用。
於是我決定把這段小插曲寫下來:如何在沒有 fancy 工具的情況下,利用 Bash + curl + nc 自己拼湊一套監控與告警系統。這麼有效率的寫code,「當然是交給羊駝來幫忙整理成能跑的程式碼。」。
這時候扔出寶貝球大喊「就決定是你了,阿爾宙斯」!!
curl -v \
--url smtp://<SMTP_HOST>:<PORT> \
-u "<USERNAME>:<PASSWORD>" \ # (可選) 若需要驗證
--mail-from "<SENDER_EMAIL>" \
--mail-rcpt "<RECIPIENT_EMAIL>" \
-T <MESSAGE_FILE>
參數 | 說明 |
---|---|
-v |
顯示詳細連線資訊(debug) |
--url smtp://… |
指定 SMTP 伺服器、port(預設 25) |
-u |
SMTP AUTH:使用 user:pass 進行登錄 |
--mail-from |
寄件人地址 |
--mail-rcpt |
收件人地址(可多次使用) |
-T |
讀取一個檔案作為完整 MIME 信件(headers + body) |
監控不就是 keep alive 或 nc -zv
嗎?
表面上是這樣,但實務上卻一堆眉角:
\r\n
一定會咬你一口。這是我在大同大學機房使用的版本。特色是:避免洗版每5分鐘才會重複告警一次。
#!/bin/bash
# send_telegram.sh (sanitized)
# 說明:主機清單不放在腳本內,改從外部檔案讀入;BOT / CHAT 為佔位符。
# 外部主機清單檔案(每行一個 host:port,例如 example.com:443)
HOSTS_FILE="${HOME}/monitor_hosts.txt"
# 檢查 host list 是否存在
if [[ ! -f "$HOSTS_FILE" ]]; then
echo "❌ 主機清單不存在:$HOSTS_FILE"
echo "請建立檔案,每行格式 host:port(例如 example.com:443)"
exit 1
fi
# 載入 host list 到陣列
declare -A HOSTS
while IFS=: read -r host port; do
# 忽略空行與註解行(以 # 開頭)
[[ -z "$host" || "$host" =~ ^# ]] && continue
HOSTS["$host"]=$port
done < "$HOSTS_FILE"
# 檢查是否有載入
if [[ ${#HOSTS[@]} -eq 0 ]]; then
echo "❌ 未解析到任何主機,請檢查 $HOSTS_FILE"
exit 1
fi
# 設定檢查間隔時間(秒)
CHECK_INTERVAL=300
# Telegram 設定(請在環境變數或 ~/.monitor_env 檔案中設定)
# BOT_TOKEN 與 CHAT_ID 不要寫在腳本裡面
BOT_TOKEN="${BOT_TOKEN:-}"
CHAT_ID="${CHAT_ID:-}"
if [[ -z "$BOT_TOKEN" || -z "$CHAT_ID" ]]; then
echo "❌ 請先在環境變數或 ~/.monitor_env 設定 BOT_TOKEN 與 CHAT_ID"
echo "例如: export BOT_TOKEN='xxxx'; export CHAT_ID='-100123...' "
exit 1
fi
declare -A ALERT_SENT
timestamp() { echo "$(date +"%Y-%m-%d %H:%M:%S")"; }
send_telegram_notify() {
local message="$1"
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "parse_mode=Markdown" \
-d "text=${message}" > /dev/null
}
while true; do
echo "$(timestamp) 🔍 開始檢查主機狀態..."
for HOST in "${!HOSTS[@]}"; do
PORT=${HOSTS[$HOST]}
if nc -zv "$HOST" "$PORT" &>/dev/null; then
echo "$(timestamp) ✅ $HOST:$PORT open"
if [[ ${ALERT_SENT[$HOST]} ]]; then
send_telegram_notify "✅ *恢復正常*\n🔹 *主機:* \`$HOST\`\n🔹 *端口:* \`$PORT\`"
unset ALERT_SENT[$HOST]
fi
else
echo "$(timestamp) ❌ $HOST:$PORT down"
if [[ -z ${ALERT_SENT[$HOST]:-} ]]; then
send_telegram_notify "🔥 *警告! 服務異常*\n🔹 *主機:* \`$HOST\`\n🔹 *端口:* \`$PORT\`\n⚠️ 無法連線,請立即檢查!"
ALERT_SENT[$HOST]=1
fi
fi
done
sleep "$CHECK_INTERVAL"
done
主管覺得 Mail 比較直覺,於是改成 M365 SMTP。
#!/usr/bin/env bash
# send_mail.sh
# 用法:
# ./send_mail.sh 收件人 主旨 內文
set -euo pipefail
# ==== SMTP 設定(範例用,請自行替換)====
SMTP_HOST="smtp.office365.com"
SMTP_PORT=587
FROM_ADDR="example@yourdomain.com" # 寄件人帳號
LOGIN_USER="example@yourdomain.com" # 登入帳號
PASS="your_password_here" # 建議用檔案讀取,不要硬編碼
# ==== 引數 ====
TO="${1:-}"
SUBJECT="${2:-}"
BODY="${3:-}"
if [ -z "$TO" ] || [ -z "$SUBJECT" ] || [ -z "$BODY" ]; then
echo "❌ 用法錯誤:請輸入 收件人 主旨 內文"
exit 1
fi
# ==== 組信 ====
TMP_MAIL="$(mktemp /tmp/mail.XXXXXX)"
trap 'rm -f "$TMP_MAIL"' EXIT
{
echo "From: ${FROM_ADDR}"
echo "To: ${TO}"
echo "Subject: ${SUBJECT}"
echo "MIME-Version: 1.0"
echo "Content-Type: text/plain; charset=UTF-8"
echo
echo "${BODY}"
} > "$TMP_MAIL"
# ==== 寄送 ====
curl --url "smtp://${SMTP_HOST}:${SMTP_PORT}" \
--mail-from "${FROM_ADDR}" \
--mail-rcpt "${TO}" \
--upload-file "$TMP_MAIL" \
--user "${LOGIN_USER}:${PASS}" \
--ssl-reqd \
--silent --show-error --verbose
echo "✅ 郵件已送出 → ${TO}"
send_mail_monitor.sh
改呼叫send mail 。
#!/usr/bin/env bash
# send_mail_monitor.sh
# 說明:監控多台主機的 TCP 連線(nc),並對 GEPTKids API 額外進行 HTTP 200 檢查(curl)。
# 依照「首次告警、恢復再通知」的原則發送信件,避免洗版。
set -euo pipefail
########################################
# 基本設定
########################################
MAIL_TO="fanli@o365.ttu.edu.tw"
SUBJECT_DOWN_TCP="🔥 斷線告警 - TCP"
SUBJECT_UP_TCP="✅ 恢復通知 - TCP"
SUBJECT_DOWN_HTTP="🔥 斷線告警 - HTTP"
SUBJECT_UP_HTTP="✅ 恢復通知 - HTTP"
# 主機清單(可按需增刪)
# 備註:api.xxx.org.tw 會同時做 TCP 與 HTTP 200 檢查
declare -A HOSTS=(
["xxx.tw"]=443
["api.xxx.org.tw"]=443 # GEPTKids API - Fuzzy Search
)
# 檢查間隔秒數
CHECK_INTERVAL=300
# 告警狀態記錄(同一輪程式執行期間)
# 一般主機:用 ALERT_SENT["host"] 記錄 TCP 告警
# API 主機:用 ALERT_SENT["host:tcp"] 與 ALERT_SENT["host:http"] 分別記錄兩種層級的告警
declare -A ALERT_SENT
########################################
# 公用函式
########################################
timestamp() { date +"%Y-%m-%d %H:%M:%S"; }
send_mail() {
local to="$1" subject="$2" body="$3"
./send_mail.sh "$to" "$subject" "$body"
}
log() {
# 統一輸出格式
echo "$(timestamp) $*"
}
########################################
# 檢查:TCP 連線 (nc)
########################################
check_tcp() {
local host="$1" port="$2"
if nc -zv "$host" "$port" &>/dev/null; then
log "✅ ${host} TCP ${port} is open"
if [[ -n "${ALERT_SENT[${host}]:-}" ]]; then
# 若之前發過異常告警,現在恢復就寄出「恢復通知」
local body=$"主機:${host}\n端口:${port}\n檢查方式:TCP 連線\n狀態:已恢復\n時間:$(timestamp)"
send_mail "$MAIL_TO" "$SUBJECT_UP_TCP" "$body"
unset "ALERT_SENT[${host}]"
fi
else
log "❌ ${host} TCP ${port} is closed or host is down"
if [[ -z "${ALERT_SENT[${host}]:-}" ]]; then
# 首次偵測到異常才寄一封,避免洗版
local body=$"主機:${host}\n端口:${port}\n檢查方式:TCP 連線\n狀態:連線失敗\n時間:$(timestamp)\n請立即檢查。"
send_mail "$MAIL_TO" "$SUBJECT_DOWN_TCP" "$body"
ALERT_SENT["${host}"]=1
fi
fi
}
########################################
# 檢查:API HTTP 回應 (curl)
# 專給 api.geptkids.org.tw:判定 200 為正常,其餘(含逾時/失敗)視為異常
########################################
check_api_http_200() {
local host="$1"
local url="https://api.geptkids.org.tw/api/fuzzySearch?q=apple"
# 超時參數避免卡住整輪檢查;失敗時統一回傳 000 方便判斷
local http_code
http_code=$(curl -sS -o /dev/null \
--connect-timeout 5 --max-time 8 \
-w "%{http_code}" "$url" || echo "000")
if [[ "$http_code" == "200" ]]; then
log "✅ ${host} API 回應 200"
if [[ -n "${ALERT_SENT[${host}:http]:-}" ]]; then
# 若之前發過異常告警,現在恢復就寄出「恢復通知」
local body=$"主機:${host}\n檢查方式:HTTP API\n狀態:已恢復(200)\n時間:$(timestamp)"
send_mail "$MAIL_TO" "$SUBJECT_UP_HTTP" "$body"
unset "ALERT_SENT[${host}:http]"
fi
else
log "❌ ${host} API 回應 ${http_code}"
if [[ -z "${ALERT_SENT[${host}:http]:-}" ]]; then
# 首次偵測到異常才寄一封,避免洗版
local body=$"主機:${host}\n檢查方式:HTTP API\n狀態:異常(回應碼:${http_code})\n時間:$(timestamp)\n請立即檢查。"
send_mail "$MAIL_TO" "$SUBJECT_DOWN_HTTP" "$body"
ALERT_SENT["${host}:http"]=1
fi
fi
}
########################################
# 主迴圈
########################################
while true; do
log "🔍 開始檢查主機狀態..."
for host in "${!HOSTS[@]}"; do
port="${HOSTS[$host]}"
if [[ "$host" == "api.geptkids.org.tw" ]]; then
# API 主機同時做兩種檢查:
# 1) TCP 連線可達(保留原本 nc -zv 行為)
# 2) HTTP 服務回應是否為 200
# TCP 使用一般主機的 ALERT_SENT["host"] 旗標
check_tcp "$host" "$port"
# API 層使用獨立的 ALERT_SENT["host:http"] 旗標
check_api_http_200 "$host"
else
# 其他主機維持原本 nc 檢查
check_tcp "$host" "$port"
fi
done
sleep "$CHECK_INTERVAL"
done
API 服務用 curl -w "%{http_code}"
驗證 200,其他服務則照樣用 nc -zv
。
去年踩過的坑,今年還是踩:
Windows 編輯的 shell script 拿到 Linux 跑,結果因為 \r
換行字元,整個腳本跑不動。
解法:
sed -i 's/\r$//' send_mail_monitor.sh
結果還沒幾天,主管又寄了一封信:
之前同事研究,網站的監控軟體,後來架設了 Uptime Kuma。
GitHub 專案:https://github.com/louislam/uptime-kuma
我:……
本來以為只是小小的監控,結果竟然讓我發現:
我愣住了。這不就是一個 橫向移動的大門 嗎?
擁有這組帳密,代表:
這比 nc -zv
探測到的 port 還要嚴重一百倍。
結果和上次一樣:我發現的,比我想解決的還要更棘手。
我開始懷疑這是不是我的宿命——永遠在不經意間看到不該看的東西。
不過話說回來,這些腳本至少能留下來,讓後來人少走一點冤枉路。